<?php
/**
 * @author Sergey Dostovalov
 */
require_once 'MarcField.php';
require_once 'MarcControlField.php';
require_once 'MarcDataField.php';
require_once 'MarcConstants.php';
require_once 'MarcException.php';
class MarcRecord {
    protected $fields = array();
    protected $leader;

    /**
     *
     * @param string $rawMarc
     * raw marc record
     *
     * @param string $rawEncoding
     * encoding of raw marc record . Default UTF-8.
     * Internal encoding UTF-8
     * List of suuported encoding See http://www.php.net/manual/en/function.mb-list-encodings.php
     * main: UTF-8, Windows-1251, KOI8-R, CP866 (DOS), ISO-8859-1
     * @exception Exception
     */
    public function  __construct($rawMarc = null, $rawEncoding = 'UTF-8')
    {
        if(array_search($rawEncoding, mb_list_encodings()) === false){
            throw new Exception('Encoding '.$rawEncoding.' not supported. See http://www.php.net/manual/en/function.mb-list-encodings.php');
        }
        if ($rawMarc !== null and strlen($rawMarc)){
            try{
                $this->decode($rawMarc, $rawEncoding);
                unset($rawMarc);
            }
            catch(MarcParseException $e){
                throw $e;
            }
        }
        $this->leader = str_repeat(' ', 24);
    }
    /**
     * Make marc record from array
     * @param array $record
     * $record = array(
     *   'ldr'=>'leader', //not necessarily
     *   '001'=>array('control field'), //control field
     *   '200'=>array(
     *      array('i1'=>'0', 'i2'=>'1', 'a'=>array('first field a', 'second field a')),
     *      array('i1'=>'0', 'i2'=>'1', 'b'=>array('first field b', 'second field b'))
     *  ),
     * );
     * @param bool $resetRecord if true record will be cleared
     * @throw MarcException
     */
    public function fromArray(array $record, $resetRecord = true){
        if ($resetRecord === true){
            $this->fields = array();
        }
        if (isset($record['ldr'])){
            if(is_string($record['ldr']) === false){
                throw new Exception('Wrong leader format. Must be as string');
            }
            $this->setLeader($record['ldr']);
            unset($record['ldr']);
        }
        foreach($record as $fieldTag=>$fieldGroup){
            $this->addFieldAsArray($fieldTag, $fieldGroup);
        }unset($fields);
    }
    /**
     * add fields to record
     * @param array $fields array of MarcControlField or MarcDataField
     */
    public function addFields(array $fields)
    {
        foreach($fields as $field){
            $this->addField($field);
        }
    }
    /**
     * add field to record
     * @param MarcField $field
     */
    public function addField(MarcField $field)
    {
        $this->fields[$field->getTag()][] = $field;
    }
    /**
     *
     * @param string $fieldTag
     * @param array $field data
     * fied data for control fields:
     *  array('001 control field text'[,'second 001 controlfield text'])
     * fied data for data fields:
     *  array('i1'=>'0', 'i2'=>'1', 'a'=>array('first subfield a', 'second subfield a'), 'b'=>array('subfield b')
     * indicators is not necessarily
     * @throw MarcException
     */
    public function addFieldAsArray($fieldTag, array $field){
        if ($fieldTag < 10){
            foreach($field as $fieldData){
                if (is_string($fieldData) === false)
                    throw new MarcException('wrong control field data format. '.$fieldTag);
                $this->addField(new MarcControlField(sprintf('%03d', $fieldTag), $fieldData));
            }unset($field);
        }
        //if data field
        if ($fieldTag > 9){
            foreach($field as $fieldData){
                if (is_array($fieldData) === false)
                    throw new MarcException('Wrong field format. Must be as array');

                $i1 = null;
                $i2 = null;

                if (isset($fieldData['i1'])) $i1 = $fieldData['i1'];
                unset($fieldData['i1']);
                if (isset($fieldData['i2'])) $i2 = $fieldData['i2'];
                unset($fieldData['i2']);
                if (count($fieldData) === 0)
                    throw new MarcException('Field not have subfields '.$fieldTag);

                foreach($fieldData as $code=>$subfieldGroup){
                    if (is_array($subfieldGroup) === false)
                        throw new MarcException('Wrong subfields format. Must be as array. '.$fieldTag);
                    $subfields = array();
                    foreach($subfieldGroup as $subfieldData){
                        $subfields[] = new MarcSubfield($code, $subfieldData);
                    }unset($subfieldData);
                    $this->addField(new MarcDataField(sprintf('%03d', $fieldTag), $subfields, $i1, $i2));
                }unset($subfieldGroup);
            }unset($fieldData);
        }
    }
    /**
     * @return array of MarcField objects
     * @return null if not have field
     * @exception MarcFieldNotExsist
     */
    public function getField($tag)
    {
        if (isset($this->fields[$tag])) return $this->fields[$tag];
        else return null;
    }
    /**
     * @return array of MarcField objects grouped by tags
     * e.g. array('245'=>array(field1, field2))
     * @return null if fields not exist
     */
    public function getFields()
    {
        if (count($this->fields) === 0) return null;
        return $this->fields;
    }
    /**
     * @param string field tag
     * @return MarcField first field in array of fields grouped by tag
     * @return null if not have field
     */
    public function getSingleField($tag)
    {
        if (isset($this->fields[$tag])) return $this->fields[$tag][0];
        else return null;
    }
    /**
     *
     * @return string record leader
     */
    public function getLeader()
    {
        return $this->leader;
    }
    /**
     *
     * @param string $leader record leader
     */
    public function setLeader($leader)
    {
        $this->leader = $leader;
    }
    /**
     * @return int number of fields that were deleted
     */
    public function deleteFields($tag)
    {
        $count = 0;
        if (isset($this->fields[$tag])){
            $count = count($this->fields[$tag]);
            unset($this->fields[$tag]);
        }
        return $count;
    }
    /**
     * Check for the record has a field with $tag
     * @param string $tag tag of filed
     * @return bool 
     */
    public function isHasField($tag){
        if (isset($this->fields[$tag])) return true;
        return false;
    }
    /**
     *
     * @param string $rawMarc
     * 
     * @param string $rawEncoding encoding of raw marc
     * @exception MarcParseException
     */
    private function decode(&$rawMarc, $rawEncoding = 'UTF-8')
    {
        /*
         * 'a' in position 9 in leader
         * determines that the record contains multibyte encoding.
         * Default record encoding UTF-8
         */
        $marcLength = strlen($rawMarc);

        $recordLength = (int)substr($rawMarc, 0, 5);
        if ($recordLength !== $marcLength){
            throw new MarcParseException(MarcParseException::WRONG_RECORD_LENGTH);
        }
        if (substr($rawMarc, -1, 1) !== MarcConstants::END_OF_RECORD){
            throw new MarcParseException(MarcParseException::INVALID_RECORD_TERMINATOR);
        }
        $this->leader = substr($rawMarc,0, MarcConstants::LEADER_LEN);
        if (strlen($this->leader) != MarcConstants::LEADER_LEN){
            throw new MarcParseException(MarcParseException::WRONG_LEADER);
        }
        $baseAddress = (int)substr($rawMarc, MarcConstants::DIRECTORY_ENTRY_LEN, 5);
        if($baseAddress < 1){
            throw new MarcParseException(MarcParseException::BASE_ADDRESS_NOT_FOUND);
        }
        if($baseAddress >= $marcLength){
            throw new MarcParseException(MarcParseException::BASE_ADDRESS_INVALID);
        }

        $directory = substr($rawMarc, MarcConstants::LEADER_LEN, $baseAddress - MarcConstants::LEADER_LEN - 1);

        if (substr($rawMarc, $baseAddress - 1, 1) !== MarcConstants::END_OF_FIELD){
            throw new MarcParseException(MarcParseException::DIRECTORY_NOT_FOUNDED);
	}
        if (strlen($directory) % MarcConstants::DIRECTORY_ENTRY_LEN != 0){
            throw new MarcParseException(MarcParseException::INVALID_RECORD_DIRECTORY);
        }
        $fieldTotal = strlen($directory) / MarcConstants::DIRECTORY_ENTRY_LEN;
       
        for($field_counter = 0;$field_counter < $fieldTotal; ++$field_counter){

            $entryStart = $field_counter * MarcConstants::DIRECTORY_ENTRY_LEN;
            $entryEnd = $entryStart + MarcConstants::DIRECTORY_ENTRY_LEN;
            $entry = substr($directory, $entryStart, MarcConstants::DIRECTORY_ENTRY_LEN);
            $entryTag = substr($entry, 0, 3);
            $entryLength = (int)substr($entry, 3, 4);
            $entryOffset = (int)substr($entry, 7, 5);
            $entryData = substr($rawMarc, $baseAddress + $entryOffset , $entryLength-1);
            if (!is_numeric($entryTag)){
                throw new MarcParseException(MarcParseException::TAG_ERROR);
            }
            if ((int)$entryTag < 10){
                if ($rawEncoding !== 'UTF-8'){
                    $this->addField(new MarcControlField($entryTag,
                            mb_convert_encoding($entryData, 'UTF-8', $rawEncoding)));
                            //iconv($rawEncoding, 'UTF-8', $entryData)));
                }
                else{
                    $this->addField(new MarcControlField($entryTag, $entryData));
                }           
            }
            else{
                $subfields = split(MarcConstants::SUBFIELD_INDICATOR, $entryData);
                $field = new MarcDataField($entryTag);
                $field->setIndicator(1, $subfields[0][0]);
                $field->setIndicator(2, $subfields[0][1]);
                array_shift($subfields); //remove indicators
                foreach($subfields as $subfield){
                    if ($rawEncoding !== 'UTF-8'){
                        $field->addSubfield(new MarcSubfield($subfield[0], 
                                mb_convert_encoding(substr($subfield, 1), 'UTF-8', $rawEncoding)));
                                //iconv($rawEncoding, 'UTF-8', $entryData)));
                    }
                    else{
                        $field->addSubfield(new MarcSubfield($subfield[0], str_replace("\xe2", '',substr($subfield, 1))));
                        //$field->addSubfield(new MarcSubfield($subfield[0], substr($subfield, 1)));
                    }             
                }unset($subfield);
                $this->addField($field);
            }
        }
    }
    public function toRaw($toEncoding = null)
    {
        ksort($this->fields, SORT_NUMERIC);
        list($fields, $directory, $recordLength, $baseAddress) = $this->buildDirectory($toEncoding);
        $this->setLeaderLengths($recordLength, $baseAddress);
        //if ($toEncoding === null) $this->leader[9] = 'a'; //'a' in 9 position indicate that record containt unicode string
        return $this->leader.implode('', $directory).MarcConstants::END_OF_FIELD.implode('', $fields).MarcConstants::END_OF_RECORD;
    }
    /**
     *
     * @param string $toEncoding
     * @return array array($fields, $directory, $total, $baseAddress);
     */
    private function buildDirectory($toEncoding = null)
    {
        $fields = array();
        $directory = array();
        $dataEnd = 0;
        
        foreach ($this->fields as $fields_group) {
            foreach ($fields_group as $field){
                // No empty fields allowed
                if (!$field->isEmpty()) {
                    // Get data in raw format
                    $raw = $field->toRaw($toEncoding);
                    $fields[] = $raw;

                    // Create directory entry
                    $len = strlen($raw);
                    $direntry = sprintf('%03s%04d%05d', $field->getTag(), $len, $dataEnd);
                    $directory[] = $direntry;
                    $dataEnd += $len;
                }
            }unset($field);
        }unset($fields_group);

        $baseAddress =
                MarcConstants::LEADER_LEN +    // better be 24
                (count($directory) * MarcConstants::DIRECTORY_ENTRY_LEN) +
                                // all the directory entries
                1;              // end-of-field marker

        $total =
                $baseAddress +  // stuff before first field
                $dataEnd +      // Length of the fields
                1;              // End-of-record marker
        if ($toEncoding === null) $this->leader[9] = 'a'; //'a' in 9 position indicate that record containt unicode string
        return array($fields, $directory, $total, $baseAddress);
    }
    /**
     *
     * @param int $recordLength
     * @param int $baseAddress
     * @return bool
     */
    public function setLeaderLengths($recordLength, $baseAddress)
    {
        if (!is_int($recordLength)) {
            return false;
        }
        if (!is_int($baseAddress)) {
            return false;
        }

        // Set record length
        $this->leader = substr_replace($this->leader, sprintf('%05d', $recordLength), 0, 5);
        $this->leader = substr_replace($this->leader, sprintf('%05d', $baseAddress), 12, 5);
        $this->leader = substr_replace($this->leader, '22', 10, 2);
        $this->leader = substr_replace($this->leader, '4500', 20, 4);
        return true;
    }
    /**
     *
     * @param string $toEncoding
     * @return string
     */
    public function  __toString($toEncoding = null) {
        ksort($this->fields, SORT_NUMERIC);
        list($fields, $directory, $recordLength, $baseAddress) = $this->buildDirectory($toEncoding);
        $this->setLeaderLengths($recordLength, $baseAddress);
        $formatted = 'LDR ' . $this->leader . "\n";
        foreach ($this->fields as $fields_group) {
            foreach($fields_group as $field){
                if (!$field->isEmpty()) {
                    $formatted .= $field->__toString($toEncoding) . "\n";
                }
            }unset($field);
            
        }unset($field_group);
        return $formatted;
    }

}
?>
